//package details
package pxgrid_controllers;

//imports
import java.io.*;
import java.util.*;
import java.math.*;
import javax.xml.bind.*;
import pxsubmission.jaxb.*;
import pxgrid_utils.PXUtils;
import pxgrid_entities.*;

/**
 * PXSubmissionController
 * A class that takes user input, and converts it into PXSubmission entities ready
 * to send to grid controllers for processing.
 * It also handles the retrieving of results from the grid controllers to the user
 * interface.
 *
 * @author Mark Bate
 */
public class PXSubmissionController {

        
    //controller variables
    private String data_store; //location that job data will be stored at
    private PXXGridController xgrid_controller; //a grid controller to play with
    private Boolean debug; //defines whether we're in debug mode

    /**
     * Creates a new instance of PXSubmissionController,
     * which will set the debug, xgrid_controller and data_store variables
     * 
     * @param debug Boolean set to true to toggle debug flags
     * @throws java.lang.Exception thrown if can't read data_store from server_config.xml
     */
    public PXSubmissionController(Boolean debug) throws Exception {
        this.debug = debug;
        this.xgrid_controller = new PXXGridController(this.debug);
        this.data_store = PXUtils.getDataStoreFromServerConfigXML();
        
        
        //if the data_store is empty, throw an exception
        if(this.data_store.matches("")) {
            throw new Exception("data_store could not be set from server_config.xml");
        } else {
            //if there's something there, check it's a real file
            File ds = new File(this.data_store);
            //if it's not a real directory throw an exception
            if(!ds.isDirectory()) {
                throw new Exception("data_store could not be set from server_config.xml");
            }
        }
    }
    
    /**
     * Processes a submission.
     * Essentially wraps a few methods and submits to the grid.
     * 
     * @param mtz_file String the MTZ file location
     * @param mtz_temp_dir String temporary directory that MTZ file is stored in
     * @param pdbs ArrayList<String> the PDB files to use as search ensembles
     * @param pdbs_temp_dir String temporary directory that PDB files are stored in
     * @param space_groups ArrayList<String> the space groups to search within
     * @param rmsds ArrayList<Double> RMS values to try
     * @param f_label String the F LABEL of the MTZ file
     * @param sigf_label String the SIGF LABEL of the MTZ file
     * @param molecular_weight double the molecular weight
     * @param number_of_molecules_in_asymmetric_unit int 
     * @param number_of_copies_of_ensemble_to_search_for int
     * @param packing int
     * @param grid_name String display name of the grid
     * @param grid_type String the type of grid to use
     * @return String a unique submission_id
     * @throws java.lang.Exception thrown if it can't find F & SIGF labels, or
     * if there's no RFREE flag, or if submission directories could not be created.
     */
    public String processSubmission(String mtz_file,
                                    String mtz_temp_dir,
                                    ArrayList<String> pdbs,
                                    String pdbs_temp_dir,
                                    String[] space_groups,
                                    String[] rmsds,
                                    String f_label,
                                    String sigf_label,
                                    double molecular_weight,
                                    int number_of_molecules_in_asymmetric_unit,
                                    int number_of_copies_of_ensemble_to_search_for,
                                    int packing,
                                    String grid_name,
                                    String grid_type) 
                                    throws Exception {
        //DEBUG STATEMENT
        if(this.debug) {
                System.out.println("PXSubmissionController:processSubmission()# BEGIN");
            }
        //DEBUG STATEMENT
        if(this.debug) {
                System.out.println("PXSubmissionController:processSubmission()# check PDB files exist");
            }
        
        //check the PDB files exist,
        //i'd check mtz too, but we already did that in the upload
        for (int i=0; i < pdbs.size(); i++) {
            String tempPDB = pdbs.get(i);
            File pdbFile = new File(pdbs_temp_dir+"/"+tempPDB);
            if(!pdbFile.exists()) {
                throw new Exception("PDB file '"+tempPDB+"' does not exist");
            }
        }

        //DEBUG STATEMENT
        if(this.debug) {
            System.out.println("PXSubmissionController:processSubmission()# generateSubmissionID()");
        }
        
        //get ourselves a nice unique ID
        String submission_id = this.generateSubmissionID();
        
        //DEBUG STATEMENT
        if(this.debug) {
            System.out.println("PXSubmissionController:processSubmission()# create a PXSubmission entity");
        }
        
        //create the submission object
        PXSubmission submission = new PXSubmission();
        
        //set the PXSubmission attributes
        submission.setDirectoryPath(data_store);
        submission.setSubmissionID(submission_id);
        
        //data store for this submission
        String submissionDir = data_store+"/"+submission_id;
        
        //DEBUG STATEMENT
        if(this.debug) {
            System.out.println("PXSubmissionController:processSubmission()# create directories");
        }
        
        //try to make the directories, if it doesn't work, throw a tantrum (or Exception)
        if(!this.createDirectoriesForSubmission(submission_id)) {
            //throw new Tantrum("Could not create directories required for submission. wah!");
            throw new Exception("Could not create directories required for submission");
        }

        //DEBUG STATEMENT
        if(this.debug) {
            System.out.println("PXSubmissionController:processSubmission()# move the MTZ File");
        }
        
        
        File mtzFile = new File(mtz_temp_dir+"/"+mtz_file); //the full path of MTZ file
        File mtzDir = new File(mtz_temp_dir); //the folder MTZ is in
        File mtzFileMoveTo = new File(submissionDir+"/"+mtz_file); //where to move the MTZ to
        
        //move the mtz file to its new location, or throw Exception
        if(!mtzFile.renameTo(mtzFileMoveTo)) {
            throw new Exception("Could not move MTZ File from temporary location to required location");
        }
        //delete the old mtz directory
        mtzDir.delete();
        
        //DEBUG STATEMENT
        if(this.debug) {
            System.out.println("PXSubmissionController:processSubmission()# move the PDB files");
        }
        
        //move the pdbs
        for (int i=0; i < pdbs.size(); i++) {
            String pdb_file = pdbs.get(i);
            File pdbFile = new File(pdbs_temp_dir+"/"+pdb_file); //where PDB currently lives
            File pdbFileMoveTo = new File(submissionDir+"/"+pdb_file); //PDB's new home
            //move the PDB or throw Exception
            if(!pdbFile.renameTo(pdbFileMoveTo)) {
                throw new Exception("Could not move PDB File '"+pdb_file+"' from temporary location to required location");
            }
        }
        
        //nuke the pdbs_temp_dir
        //if we iterate through then we clean out the tar file, and any other
        //random crap in the tar archive.
        File pdbDir = new File(pdbs_temp_dir);
        File[] pdbDirFiles = pdbDir.listFiles();
        for(int i=0; i < pdbDirFiles.length; i++) {
            pdbDirFiles[i].delete();
        }
        pdbDir.delete();
        
        //DEBUG STATEMENT
        if(this.debug) {
            System.out.println("PXSubmissionController:processSubmission()# begin creating PXJobs");
        }
        
        
        //cycle through pdbs, spacegroups & rmsds, creating the relevant jobs.
        // a 3 dimensional array, travelling through space groups and time...
        //Formula: no. of jobs = no. of pdbs * no. of space_groups * no. of rmsds
        for(int i=0; i < pdbs.size(); i++) {
            for(int j=0; j < space_groups.length; j++) {
                for(int k=0; k < rmsds.length; k++) {
                    
                    //create the job object
                    PXJob job = new PXJob();
                    String jobName = submission_id+"_"+mtz_file+"_"+pdbs.get(i)
                                    +"_"+space_groups[j]+"_"+rmsds[k];
                    
                    //set the job attributes
                    job.setJobName(jobName);
                    job.setInputMTZ(mtz_file);
                    job.setInputPDB(String.valueOf(pdbs.get(i)));
                    job.setSpaceGroup(String.valueOf(space_groups[j]));
                    job.setRMSD(Double.parseDouble(rmsds[k]));
                    job.setFLabel(f_label);
                    job.setSigfLabel(sigf_label);
                    job.setMolecularWeight(molecular_weight);
                    job.setNumberOfMoleculesInAsymmetricUnit(number_of_molecules_in_asymmetric_unit);
                    job.setNumberOfCopiesOfEnsembleToSearchFor(number_of_copies_of_ensemble_to_search_for);
                    job.setPacking(packing);
                    job.setGridController(grid_name);
                    job.setGridType(grid_type);
                    
                    //DEBUG STATEMENT
                    if(this.debug) {
                        System.out.println("PXSubmissionController:processSubmission()# add PXJob to PXSubmission");
                    }
                    
                    //add the PXJob to the PXSubmission
                    submission.addJob(job);
                }
            }
        }
        
        //DEBUG STATEMENT
        if(this.debug) {
            System.out.println("PXSubmissionController:processSubmission()# finish creating jobs");
        }
        
        //check what type of grid we're submitting to.
        if(grid_type.equalsIgnoreCase("xgrid")) {
            
            //DEBUG STATEMENT
            if(this.debug) {
                System.out.println("PXSubmissionController:processSubmission()# submitJobsToGrid");
            }
            
            //Submit the PXSubmission to the grid
            //this should repopulate PXSubmission with grid_job_ids for the PXJobs
            submission = xgrid_controller.submitJobsToGrid(submission);
            
            try {
                //write a little XML file describing the submission, so we can
                //get stuff back later
                writeSubmissionXML(submission);
            } catch (Exception ex) {
                throw new Exception("submission_id was not set for PXSubmission, cannot write Submission XML");
            }
                
        }
        
        //DEBUG STATEMENT
        if(this.debug) {
            System.out.println("PXSubmissionController:processSubmission()# END");
        }
        
        //return the submission_id
        return submission_id;
    }
    
    /**
     * Retrieves the results of jobs linked to submission id
     * @param submission_id String the unique ID of the submission to get results of
     * @return PXSubmission entity containing the jobs with data filled in
     */
    public PXSubmission getResultsForSubmissionID(String submission_id) {
        
        //DEBUG STATEMENT
        if(this.debug) {
                System.out.println("PXSubmissionController:processSubmission()# BEGIN");
            }
        //DEBUG STATEMENT
        if(this.debug) {
                System.out.println("PXSubmissionController:processSubmission()# set directories");
            }
        
        //define the submission directory for this submission
        String submission_dir = this.data_store+"/"+submission_id;
        //the xml file describing the submission
        String submission_data_file = submission_dir+"/PXSubmission.xml";
        
        //DEBUG STATEMENT
        if(this.debug) {
            System.out.println("PXSubmissionController:processSubmission()# create PXSubmission from XML");
        }
        
        //populate a submission object from the xml file
        PXSubmission submission = PXUtils.createPXSubmissionEntityFromXML(submission_data_file);
        submission.setDirectoryPath(submission_dir);
        
        //DEBUG STATEMENT
        if(this.debug) {
            System.out.println("PXSubmissionController:processSubmission()# get the PXJobs");
        }
        
        //get the jobs of the submission
        ArrayList<PXJob> jobs = submission.getJobs();
        
        //DEBUG STATEMENT
        if(this.debug) {
            System.out.println("PXSubmissionController:processSubmission()# cycle the PXJobs");
        }
        
        
        //cycle the jobs
        for(PXJob job : jobs) {
            
            //DEBUG STATEMENT
            if(this.debug) {
                System.out.println("PXSubmissionController:processSubmission()# create instance of PXJob");
            }
            
            //DEBUG STATEMENT
            if(this.debug) {
                System.out.println("PXSubmissionController:processSubmission()# check grid type");
            }
            
            //check that we're using xgrid as our grid type
            if(job.getGridType().toLowerCase().matches("xgrid")) {
                
                //try to get the attributes of the job from the grid
                try {
                    
                    //DEBUG STATEMENT
                    if(this.debug) {
                        System.out.println("PXSubmissionController:processSubmission()# get job attributes");
                    }
                    
                    //try & get the status and other attributes back
                    job = this.xgrid_controller.getJobAttributes(job);
                    
                    //DEBUG STATEMENT
                    if(this.debug) {
                        System.out.println("PXSubmissionController:processSubmission()# got job attributes");
                    }
                    
                //    
                } catch (Exception ex) {
                    
                    //DEBUG STATEMENT
                    if(this.debug) {
                        System.out.println("PXSubmissionController:processSubmission()# couldn't get job attributes");
                    }
                    
                    ex.printStackTrace();
                    
                    //maybe we should do something with the exception,
                    //but for the moment, lets set job status to error.
                    job.setStatus("error retrieving job attributes");
                }

                //try and get the results back for the jobs
                try{
                    //DEBUG STATEMENT
                    if(this.debug) {
                        System.out.println("PXSubmissionController:processSubmission()# get the results");
                    }

                    job = this.xgrid_controller.getJobResults(job, submission_dir+"/results");

                } catch (Exception ex) {

                    //DEBUG STATEMENT
                    if(this.debug) {
                        System.out.println("PXSubmissionController:processSubmission()# couldn't get results");
                    }

                    //maybe we should do something with the exception,
                    //but for the moment, lets set job status to error.
                    job.setStatus("error retrieving results");
                }
                    
            }

        }
        
        //DEBUG STATEMENT
        if(this.debug) {
            System.out.println("PXSubmissionController:processSubmission()# END");
        }
        
        //return the PXSubmission
        return submission;
    }
    
    /**
     * Generates a random 8 character string to be used as a submission ID
     * If the submission ID already exists in directory_to_store_data then
     * generate another ID.
     * Not quite as awesome as Apple's uuidgen function with it's "unique across
     * space and time", but it does the job, plus 8 characters is easier to remember
     * than 32.
     * @return String an 8 character ID, that should be unique within the data_store
     */
    public String generateSubmissionID() {

        //start with a blank String
        String submission_id = "";
        //grab an ArrayList of existing folders in the data_store
        ArrayList existing_submission_ids = PXUtils.getDirectoryContentsAsArrayList(this.data_store);
        
        //cycle till we have something that doesn't already exist & isn't blank
        while(submission_id.matches("") || existing_submission_ids.contains(submission_id)) {
            
            for(int i=0; i < 8; i++) {
            
                //give us a random int between 0 and 61,
                //this is enough for numbers 0-9, and the alphabet both lower & upper case
                Random generator = new Random();
                int r = generator.nextInt(61);

                /*
                 * valid characters to use are 0-9, A-Z & a-z.
                 * $r = 10 translates as A (ASCII 65),
                 * $r = 35 translates as Z (ASCII 90),
                 * $r = 36 translates as a (ASCII 97),
                 * $r = 61 translates as z (ASCII 122).
                 * we use char & a bit of arithmetic to convert values
                 * greater than 9 to ASCII characters.
                 */
                 
                if(r < 10) {
                    submission_id += r;
                }
                else if (r < 36) {
                    submission_id += (char) (55+r);
                }
                else {
                    submission_id += (char) (61+r);
                }
            }
        }
        //return our 8 character unique string
        return submission_id;
    }
    
    
    /**
     * Creates the directories required for the submission,
     * namely data_store/submission_id/ and data_store/submission_id/results/
     * @param submission_id the unique ID to use as the folder name
     * @return Boolean true if the directories were made, else false
     */
    public Boolean createDirectoriesForSubmission(String submission_id) {
        
        File results_folder = new File(this.data_store+"/"+submission_id+"/results");
        
        //this method will make the directories required to fulfill the path. ;p
        //IE. we can create the entire thing in one go.
        results_folder.mkdirs();
        
        //check that it created fine
        if(results_folder.isDirectory()) {
            return true;
        }
        
        //if it didn't return failure
        return false;
    }
    

    /**
     * Writes an XML file describing the submission, and the required attributes
     * to get the results back.
     * 
     * @param submission PXSubmission the submission to write the XML for 
     * @return String the full path to the XML file
     */
    private String writeSubmissionXML(PXSubmission submission) throws Exception {
        
        
        String xmlname = "PXSubmission.xml"; //the file name for the XML file.
        String submission_id = submission.getSubmissionID();
        
        //check that submission_id isnt null
        if(submission_id.equals(null)) {
            throw new Exception("submission_id not set in PXSubmission");
        }
        
        //the full path to xml file
        String filename = data_store+"/"+submission_id+"/"+xmlname;
        
        
        //STEVES CODE: I believe he's using JAXB to translate entities to XML?
        try{
            JAXBContext jaxbContext = JAXBContext.newInstance("pxsubmission.jaxb");
            
            ObjectFactory objFactory = new ObjectFactory();
        
            Submission submissionXML = objFactory.createSubmission();
            
            Submission.Jobs jobs = objFactory.createSubmissionJobs();
            
            submissionXML.setSubmissionId(submission_id);
            
            // for each job in the submission...
            for(PXJob pxjob : submission.getJobs())
            {
                Submission.Jobs.Job job = objFactory.createSubmissionJobsJob();
                
                job.setJobName(pxjob.getJobName());
                job.setInputMtz(pxjob.getInputMTZ());
                job.setInputPdb(pxjob.getInputPDB());
                job.setSpaceGroup(pxjob.getSpaceGroup());
                job.setRmsd(new Float(pxjob.getRMSD()));
                job.setGridType(pxjob.getGridType());
                job.setGridDisplayName(pxjob.getGridController());
                job.setGridJobId(BigInteger.valueOf(new Long(pxjob.getGridJobID())));
                
                jobs.getJob().add(job);
            }
            
            submissionXML.setJobs(jobs);
            
            try {
                Marshaller marshaller = jaxbContext.createMarshaller();
                marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT ,
                       new Boolean(true));                 
                marshaller.marshal(submissionXML,
                new FileOutputStream(filename)); 
                
                if(this.debug)
                {
                    System.out.println("Wrote to " + filename);
                }
            }
            catch(FileNotFoundException f)
            {
                f.printStackTrace();
            }            
        }
        catch(JAXBException j)
        {
            j.printStackTrace();
        }        
        
        //return the full path to xml file
        return filename;
    }
    
    
    //THIS METHOD IS NOT YET USED, IT'S HERE FOR FUTURE RELEASES
    
    /**
     * Performs an action on a grid job
     * 
     * @param action String the action to perform
     * @param job PXJob the job to perform the action on
     * @return Boolean whether the job was performed
     */
    public Boolean performGridActionOnJob(String action, PXJob job) {
        
        try {
            
            if(!action.toLowerCase().equals("delete")) {
                Boolean performed = this.xgrid_controller.performGridActionOnJob(action, job);
                return performed;
            }
            
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        
        return false;
    }
    
}
